﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;

namespace xpe
{
    public enum BlendMode { Additive, AlphaNoSorting, AlphaWithSorting }

    public class ParticleSystem
    {
        public static readonly int MIN_PARTICLES = 8 * 8;
        public static readonly int MAX_PARTICLES = 2048 * 2048;
        protected Particle particle;
        protected ParticleEmitter emitter;
        protected RenderTarget2D[] position;
        protected RenderTarget2D[] velocity;
        protected RenderTarget2D attributes;
        protected Queue<int> indices;
        protected VertexPositionColor[] attributeData;
        protected VertexDeclaration attributeDeclaration;
        protected Effect emitterEffect;
        protected Effect renderingEffect;
        protected Effect attributeEffect;
        protected BlendMode mode;
        protected float time;
        protected int total;
        protected int max;
        protected int bufferIndex;
        protected bool isInitialized;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="particle"></param>
        /// <param name="emitter"></param>
        /// <param name="maxParticles"></param>
        public ParticleSystem(Particle particle, ParticleEmitter emitter, int maxParticles)
        {
            if (particle == null)
                throw new ArgumentNullException("particle == null");

            if (emitter == null)
                throw new ArgumentNullException("emitter == null");

            this.particle = particle;
            this.emitter = emitter;

            velocity = new RenderTarget2D[2];
            position = new RenderTarget2D[2];

            attributeData = new VertexPositionColor[0];
            indices = new Queue<int>();

            Init(maxParticles);
        }

        /// <summary>
        /// Computes the next power of two larger than n
        /// http://www.gamedev.net/community/forums/topic.asp?topic_id=229831
        /// </summary>
        /// <param name="n">The number to check</param>
        /// <returns>The next power of two larger than n or n if n is a power of two</returns>
        protected int NextPowerOfTwo(int n)
        {
            --n;

            n |= n >> 1;
            n |= n >> 2;
            n |= n >> 4;
            n |= n >> 8;
            n |= n >> 16;

            return ++n;
        }

        /// <summary>
        /// Initializes this particle system to its default values
        /// </summary>
        /// <param name="maxParticles">The max number of particles</param>
        protected virtual void Init(int maxParticles)
        {
            mode = BlendMode.Additive;

            time = 0f;

            //check the maximum number of particles
            if (maxParticles < MIN_PARTICLES)
                maxParticles = (int)Math.Sqrt(MIN_PARTICLES);

            if (maxParticles > MAX_PARTICLES)
                maxParticles = (int)Math.Sqrt(MAX_PARTICLES);

            max = NextPowerOfTwo((int) Math.Sqrt(maxParticles));
            max = max * max;

            isInitialized = false;
        }

        public virtual void InitializeSystem(GraphicsDevice device, ContentManager contentManager)
        {
            int size;

            if (isInitialized)
                return;

            size = (int)Math.Sqrt(max);

            for (int i = 0; i < position.Length; i++)
                position[i] = new RenderTarget2D(device, size, size, 1, SurfaceFormat.Vector4);

            for (int i = 0; i < velocity.Length; i++)
                velocity[i] = new RenderTarget2D(device, size, size, 1, SurfaceFormat.Vector4);

            attributes = new RenderTarget2D(device, size, size, 1, SurfaceFormat.Vector4);

            //TODO - load emitter and rendering effects

            attributeEffect = contentManager.Load<Effect>("shaders/attribute");
            attributeEffect.Parameters["world"].SetValue(Matrix.Identity);
            attributeEffect.Parameters["view"].SetValue(Matrix.CreateLookAt(new Vector3(0f, 0f, 25f), new Vector3(0f, 0f, -1f), Vector3.UnitY));
            attributeEffect.Parameters["projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, size, size, 0, 1f, 1000f));

            attributeDeclaration = new VertexDeclaration(device, VertexPositionColor.VertexElements);

            for (int i = 0; i < max; i++)
                indices.Enqueue(i);

            bufferIndex = -1;
            total = 0;
            isInitialized = true;
        }

        /// <summary>
        /// Cleares out every value in the rendering targets
        /// </summary>
        /// <param name="device">The GraphicsDevice</param>
        /// <param name="tpf">The elapsed time</param>
        protected virtual void OnFirstUpdate(GraphicsDevice device, float tpf){
            List<RenderTarget2D> targets;
            Color color;

            targets = new List<RenderTarget2D>();

            targets.AddRange(position);
            targets.AddRange(velocity);
            targets.Add(attributes);

            color = new Color(0f, 0f, 0f, 0f);

            foreach (RenderTarget2D target in targets)
            {
                device.SetRenderTarget(0, target);

                device.Clear(color);

                device.SetRenderTarget(0, null);
            }
        }

        public virtual void Update(GraphicsDevice device, float tpf)
        {
            VertexPositionColor vertex;
            float x, y, z;
            float mass, lifeTime;
            int width, size, index;

            time += tpf;

            if (bufferIndex < 0)
            {
                OnFirstUpdate(device, tpf);
                bufferIndex = 0;
            }

            //calculate the number of particles to create
            size = (int) (emitter.Rate * tpf);

            if (size > indices.Count)
                size = indices.Count;  //create as many particles as we can

            //get the width of the rendering targets
            width = attributes.Width;

            //get the propertices for each new particle
            lifeTime = particle.LifeTime;
            mass = particle.Mass;

            if (size > attributeData.Length)
                attributeData = new VertexPositionColor[size];  //enlarge the attribute data

            for (int i = 0; i < size; i++)
            {
                //get a new index from the queue
                index = indices.Dequeue();

                //calculate the position in pixel coordinates
                x = index - (width * (index / width));
                y = index / width;
                z = 0f;

                vertex = new VertexPositionColor();
                vertex.Position = new Vector3(x, y, z);  //the position with (0, 0) as the upper left hand corner
                vertex.Color = new Color(time, lifeTime, mass); //the attributes

                attributeData[i] = vertex;
            }

            //draw each point
            if (size > 0)
            {
                device.SetRenderTarget(0, attributes);

                device.VertexDeclaration = attributeDeclaration;

                attributeEffect.CurrentTechnique = attributeEffect.Techniques["CreateParticles"];

                attributeEffect.Begin();

                foreach (EffectPass pass in attributeEffect.CurrentTechnique.Passes)
                {
                    pass.Begin();
                    device.RenderState.PointSize = 1;
                    device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.PointList, attributeData, 0, size);
                    pass.End();
                }

                attributeEffect.End();

                device.SetRenderTarget(0, attributes);
            }

            total += size;

            //TODO - update velocities

            //TODO - update positions
        }

        public virtual void Render(GraphicsDevice device, Matrix viewProjection, float tpf)
        {
            //TODO - render each particle
        }

        /// <summary>
        /// Gets the particle generated by this ParticleSystem
        /// </summary>
        public Particle Particle
        {
            get { return particle; }
        }

        /// <summary>
        /// Gets the emitter for this ParticleSystem
        /// </summary>
        public ParticleEmitter Emitter
        {
            get { return emitter; }
        }

        /// <summary>
        /// The blend mode for this particle system
        /// </summary>
        public BlendMode Mode
        {
            get { return mode; }
            set { mode = value; }
        }
    }
}
